﻿local _GLOBALNAME = "rLua"
local VERSION = "1.2"
rLua = {}
local rL = rLua
local char = string.char
local byte = string.byte
local concat = table.concat
local timeOutElapsed = 0
rL.SessionEvents = { "SESSION_INC_UPDATE", "SESSION_INC_ERROR", "SESSION_INC_CLOSED", "SESSION_INC_DENIED", "SESSION_OUT_CLOSED", "SESSION_OUT_ERROR", "SESSION_OUT_UPDATE", }

function rL.Init()
	rL.Prefix = "rLO"
	rL.PIDStart = 10
	rL.PIDBase = 240
	rL.dataLen = 242	--254(Maxlen)-3(prefix)-1(\t)-4(SID)-4(PieceID)
	rL.MaxRetry = 5
	rL.Version = VERSION
	rL.GlobalSID = rL.EncodeID(0)
	rL.Sessions_Inc = {}
	rL.Sessions_Out = {}
	for _, value in pairs( rL.SessionEvents ) do
		rL[value] = function( ... )
			rL.log( value, ... )
		end
	end
	rL.ObjTypes = setmetatable( rL.ObjTypes or {}, {
		__index = function ( table, key )
			return rawget( table, type(key) == "string" and key:upper() or key )
		end,
		__newindex = function ( table, key, value )
			rawset( table, type(key) == "string" and key:upper() or key, value )
		end
	} )
	
	rL.frame = _G[ "rLCommFrame" .. VERSION ] or CreateFrame( "Frame", "rLCommFrame" .. VERSION )
	rL.frame:SetScript( "OnEvent", rL.OnEvent )

	rL.frame:SetScript( "OnUpdate", rL.OnUpdate )
	rL.frame:RegisterEvent( "CHAT_MSG_ADDON" )
	rL.frame:RegisterEvent( "VARIABLES_LOADED" )
	if not RegisterAddonMessagePrefix( rL.Prefix ) then
		rL.log( "Failed registering addon message prefix" )
	end
end

function rL.dostring( str, ... )
	local func,err = loadstring( str )
	if func then
		return pcall( func, ... )
	end
	return false, err
end

function rL.log( ... )
	-- print( "rLua:", ... )
end

function rL.NextSID( start )
	for i = (start or 1), 3317760000 do
		if not rL.Sessions_Inc[i] and not rL.Sessions_Out[i] then
			return i
		end
	end
end

function rL.EncodeID( id )
	if not id or type(id) ~= "number" or id < 0 then
		return
	end
	local chars = {}
	for i = 1, 4 do
		chars[i] = char( id % rL.PIDBase + rL.PIDStart )
		id = math.floor( id / rL.PIDBase )
	end
	return concat( chars )
end

function rL.DecodeID( code )
	if type(code) ~= "string" or code:len() ~= 4 then
		return
	end
	local id = 0
	for i = 1, 4 do
		id = id * rL.PIDBase + byte( code:sub(-i,-i) ) - rL.PIDStart
	end
	return id
end

function rL.SendCommMessage( Target, Data )
	if not Target then
		return
	end
	local Data = Data or ""
	if Data:len() <= rL.dataLen then		--Short data piece, send via global session (flag C)
		rL.SendGlobalMessage( Target, "D", Data )
		-- rL.log(("Server %s: Data sent via global session"):format(Target))
	else
		local SessionID = rL.NextSID()
		local NumPieces = ceil( Data:len() / rL.dataLen )
		rL.Sessions_Out[SessionID] = {
			Target = Target,
			Status = "REQUEST",
			DataToSend = Data,
			SessionID = SessionID,
			Total = NumPieces,
			Sent = 0,
			Retry = 0,
			time = time(),
		}
		rL.SendGlobalMessage( Target, "A", rL.EncodeID( SessionID ), rL.EncodeID( NumPieces ) )
		rL.SESSION_OUT_UPDATE( Target, SessionID )
	end
end

function rL.GetCTL( )
	return ( not rL.DisableCTL ) and ChatThrottleLib
end

function rL.SendAddonMessage( priority, ... )
	local CTL = rL.GetCTL()
	if CTL then
		return CTL:SendAddonMessage( priority, ... )
	else
		return SendAddonMessage( ... )
	end
end

function rL.SendGlobalMessage( Target, Flag, ... )
	return rL.SendAddonMessage( "NORMAL", rL.Prefix, concat{ rL.GlobalSID, Flag:sub(1,1), ... }, "WHISPER", Target )
end

function rL.SendObject( Target,Object )
	if not rL.Serialize then
		rL.log( "Error: Cannot find serializer" )
		return
	end
	local Data = rL.Serialize( Object )
	return rL.SendCommMessage( Target, Data )
end

function rL.PackData( SessionID, Data )
	local Data = Data or ""
	local pieceCount = ceil( Data:len() / rL.dataLen )
	local PackTable = {}
	for i = 1, pieceCount do
		tinsert( PackTable, rL.EncodeID( SessionID ) .. rL.EncodeID( i ) .. Data:sub( rL.dataLen * ( i - 1 ) + 1, rL.dataLen * i ) )
	end
	return PackTable
end

function rL.OnEvent( self, event, prefix, msg, channel, sender )
	if event == "CHAT_MSG_ADDON" then
		if prefix == rL.Prefix and channel == "WHISPER" then	--Msg from rLua addon
			local SessionID = rL.DecodeID( msg:match("^(....)") )
			if SessionID == 0 then
				local Flag = msg:match("^....(.)")
				local arg1 = rL.DecodeID( msg:match("^.....(....)") )
				local arg2 = rL.DecodeID( msg:match("^.........(....)") )
				if Flag == "A" then	--Request of a new session
					local ReqSessionID, NumPieces = arg1, arg2
					if rL.Sessions_Inc[ReqSessionID] then
						rL.SendGlobalMessage( sender, "b", rL.EncodeID( ReqSessionID ), rL.EncodeID( rL.NextSID() ) )	--Denies the request
						rL.SESSION_INC_DENIED( sender, ReqSessionID )
					else
						rL.Sessions_Inc[ReqSessionID] = {
							Target = sender,
							Data = {},
							Received = 0,
							Total = NumPieces,
							Status = "LISTENING",
							SessionID = ReqSessionID,
							time = time(),
						}
						rL.SendGlobalMessage( sender, "B", rL.EncodeID( ReqSessionID ), rL.EncodeID( NumPieces ) )	--Ack the request
						rL.SESSION_INC_UPDATE( sender, ReqSessionID )
					end
				elseif Flag == "B" then
					local AckSessionID, NumPieces = arg1, arg2
					local Session = rL.Sessions_Out[AckSessionID]
					if not Session then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Invalid session ID in ack" )
						return
					end
					if Session.Status ~= "REQUEST" then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Status mismatch (%s) in ack", Session.Status or "UNKNOWN" )
						return
					end
					if Session.Target ~= sender then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Target mismatch (%s req) in ack", Session.Target or "UNKNOWN" )
						return
					end
					if Session.Total ~= NumPieces then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Package length mismatch (%d req, %d ack) in ack", Session.Total or 0, NumPieces )
						return
					end
					local Datatable = rL.PackData( AckSessionID, Session.DataToSend )
					Session.Status = "SENDING"
					rL.SESSION_OUT_UPDATE( sender, AckSessionID )
					local function track( args )
						local Session, pieceID = unpack( args )
						Session.Sent = ( Session.Sent or 0 ) + 1
						Session.time = time()
						rL.SESSION_OUT_UPDATE( Session.Target, Session.SessionID )
					end
					for index = 1, #Datatable do
						rL.SendAddonMessage( "BULK", rL.Prefix,Datatable[index], "WHISPER", Session.Target, nil, track, { Session, index, } )
					end
				elseif Flag == "b" then
					local AckSessionID, Start = arg1, arg2
					local Session = rL.Sessions_Out[AckSessionID]
					if not Session then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Invalid session ID to deny" )
						return
					end
					if Session.Status ~= "REQUEST" then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Status mismatch (%s) to deny", Session.Status or "UNKNOWN" )
						return
					end
					if Session.Target ~= sender then
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Target mismatch (%s req) to deny", Session.Target or "UNKNOWN" )
						return
					end
					Session.time = time()
					Session.Retry = Session.Retry + 1
					if Session.Retry <= rL.MaxRetry then
						local NewSessionID = rL.NextSID( Start )
						Session.SessionID = NewSessionID
						rL.Sessions_Out[NewSessionID] = Session
						rL.Sessions_Out[AckSessionID] = nil
						rL.SendGlobalMessage( sender, "A", rL.EncodeID(NewSessionID), rL.EncodeID(Session.Total) )
						rL.SESSION_OUT_ERROR( sender, AckSessionID, "Session ID unavailable, retry ID %d (%d / %d)", NewSessionID, Session.Retry, rL.MaxRetry )
					else
						rL.Sessions_Out[AckSessionID] = nil
						rL.SESSION_OUT_CLOSED( sender, AckSessionID, "Session ID unavailable, communication failed" )
					end
				elseif Flag == "C" then
					local ReqSessionID, NumPieces = arg1, arg2
					local Session = rL.Sessions_Out[ReqSessionID]
					if not Session then
						rL.SESSION_OUT_ERROR( sender, ReqSessionID, "Invalid session ID to close" )
						return
					end
					if Session.Status ~= "SENDING" then
						rL.SESSION_OUT_ERROR( sender, ReqSessionID, "Status mismatch (%s) to close", Session.Status or "UNKNOWN" )
						return
					end
					if Session.Target ~= sender then
						rL.SESSION_OUT_ERROR( sender, ReqSessionID, "Target mismatch (%s req) to close", Session.Target or "UNKNOWN" )
						return
					end
					if Session.Total ~= NumPieces then
						rL.SESSION_OUT_ERROR( sender, ReqSessionID, "Package length mismatch (%d req, %d ack) to close", Session.Total or 0, NumPieces )
						return
					end
					rL.Sessions_Out[ReqSessionID] = nil
					rL.SESSION_OUT_CLOSED( sender, ReqSessionID )
				elseif Flag == "D" then
					local Data = msg:match( "^.....(.+)$" )
					rL.MsgReceived( Data, sender, 0 )
				elseif Flag == "E" then
					local AckSessionID = arg1
					if not rL.Sessions_Inc[AckSessionID] then
						rL.SESSION_INC_ERROR( sender, AckSessionID, "Invalid session ID to cancel" )
						return
					end
					rL.Sessions_Inc[AckSessionID] = nil
					rL.SESSION_INC_CLOSED( sender, AckSessionID )
				else
					rL.log( ("Client %s: Invalid flag %s in global session"):format( sender,Flag ) )
				end
			else
				local Session = rL.Sessions_Inc[SessionID]
				if not Session then
					rL.SESSION_INC_ERROR( sender, SessionID, "Invalid session ID" )
					return
				end
				local PieceID, Data = msg:match( "^....(....)(.+)$" )
				PieceID = rL.DecodeID( PieceID )
				if ( not Session.Data[PieceID] ) and PieceID <= Session.Total then
					Session.Data[PieceID] = Data
					Session.Received = Session.Received + 1
					Session.time = time()
					rL.SESSION_INC_UPDATE( sender, SessionID )
				end
				if Session.Total == Session.Received then
					for index = 1, Session.Total do
						if not Session.Data[index] then
							Session.Received = Session.Received - 1
						end
					end
					if Session.Total == Session.Received then
						local Data = concat( Session.Data )
						rL.SendGlobalMessage( sender, "C", rL.EncodeID( SessionID ), rL.EncodeID( Session.Total ) )
						rL.MsgReceived( Data, sender, SessionID )
						rL.Sessions_Inc[SessionID] = nil
						rL.SESSION_INC_CLOSED( sender, SessionID )
					end
				end
			end
		end
	elseif event == "VARIABLES_LOADED" then
		rL.log( "Loading autorun code..." )
		if not rLDB then rLDB = { code = {}, autorun = {}, } end
		rL.db = rLDB
		for key, value in pairs( rLDB.autorun ) do
			if value then
				rL.dostring( rLDB.code[key] )
			end
		end
	end
end

function rL.MsgReceived( msg, sender, SessionID )
	-- rL.log(("Client %s - Session %d: Message received:\n%s"):format(sender,SessionID,msg:len()))
	if not rL.Deserialize then rL.log( "Error: Cannot find serializer" ) return end
	
	if not rL.db then	--Since rLua 1.1 may be an update injected to a lower version, VARIAVLES_LOADED may not be fired
		if not rLDB then rLDB = { code = {}, autorun = {}, } end
		rL.db = rLDB
	end
	
	local status, obj = rL.Deserialize( msg )
	if not status then
		rL.log( ("Error: Error deserializing data: %s"):format(obj) )
		return
	end

	rL.ObjReceived( obj, sender, SessionID )
end

function rL.ObjReceived( obj, sender, SessionID )
	if not obj.Type then
		rL.log( "Error: Invalid object received" )
		return
	end
	
	local func = rL.ObjTypes[obj.Type]
	if not func then
		rL.log( ("Error: Unknown object type %s"):format( tostring(obj.Type) ) )
		return
	end
	
	local function reply(obj) rL.SendObject( sender, obj ) end
	local status, err = pcall( func, reply, obj, sender, SessionID )
	if not status then
		rL.log( ("Error processing object: %s"):format(err) )
	end
end

function rL.OnUpdate( self, elapsed )
	timeOutElapsed = timeOutElapsed + elapsed
	if timeOutElapsed > 1 then
		local time = time()
		for SessionID, Session in pairs( rL.Sessions_Out ) do
			if Session.Status == "REQUEST" and time - Session.time > 30 then
				rL.SESSION_OUT_CLOSED( Session.Target, SessionID, "Request time out" )
				rL.SendGlobalMessage( Session.Target, "E", rL.EncodeID( SessionID ) )
				rL.Sessions_Out[SessionID] = nil
			elseif Session.Status == "SENDING" and Session.Sent == Session.Received and time - Session.time > 30 then
				rL.SESSION_OUT_CLOSED( Session.Target, SessionID, "Session time out sending data. Session closed" )
				rL.Sessions_Out[SessionID] = nil
			end
		end
		for SessionID, Session in pairs( rL.Sessions_Inc ) do
			if ( time - Session.time > 60 and Session.Received > 0) or time - Session.time > 600 then
				rL.SESSION_INC_CLOSED( Session.Target, SessionID, "Listening time out" )
				rL.Sessions_Inc[SessionID] = nil
			end
		end
		timeOutElapsed = timeOutElapsed - 1
	end
end

rL.Init()
